/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.netbeans.modules.web.jsf.wizards;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import org.netbeans.api.java.classpath.ClassPath;
import org.netbeans.api.java.project.JavaProjectConstants;
import org.netbeans.api.project.ProjectUtils;
import org.netbeans.api.project.SourceGroup;
import org.netbeans.api.project.Sources;
import org.netbeans.modules.html.editor.api.gsf.HtmlParserResult;
import org.netbeans.modules.html.editor.lib.api.elements.Attribute;
import org.netbeans.modules.html.editor.lib.api.elements.OpenTag;
import org.netbeans.modules.parsing.api.ParserManager;
import org.netbeans.modules.parsing.api.ResultIterator;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.UserTask;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.web.api.webmodule.WebProjectConstants;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.netbeans.modules.web.jsf.JsfConstants;
import org.netbeans.modules.web.jsf.dialogs.BrowseFolders;
import org.netbeans.modules.web.jsf.wizards.TemplateClientPanel.TemplateEntry;
import org.netbeans.modules.web.jsfapi.api.DefaultLibraryInfo;
import org.netbeans.spi.java.classpath.ClassPathProvider;
import org.netbeans.spi.project.ui.templates.support.Templates;
import org.openide.WizardDescriptor;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileUtil;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.NbBundle.Messages;
import org.openide.util.Parameters;

/**
 *
 * @author Petr Pisl
 */

public class TemplateClientPanelVisual extends javax.swing.JPanel implements HelpCtx.Provider {

    private static final long serialVersionUID = 1L;

    private WizardDescriptor wizardDescriptor;

    private final Set<ChangeListener> listeners = new HashSet<>(1);

    private static final String VALUE_NAME = "name";    //NOI18N

    /** Creates new form TemplateClientPanel */
    public TemplateClientPanelVisual(WizardDescriptor wizardDescriptor) {
        initComponents();
        this.wizardDescriptor = wizardDescriptor;
    }
    
    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {

        bgRootTag = new javax.swing.ButtonGroup();
        jrbHtml = new javax.swing.JRadioButton();
        jrbComposition = new javax.swing.JRadioButton();
        jlRootTag = new javax.swing.JLabel();
        jlTemplate = new javax.swing.JLabel();
        jtfTemplate = new javax.swing.JTextField();
        jbBrowse = new javax.swing.JButton();
        sectionsToGenerateLabel = new javax.swing.JLabel();
        SectionsScrollPane = new javax.swing.JScrollPane();
        sectionsTable = new javax.swing.JTable();

        bgRootTag.add(jrbHtml);
        jrbHtml.setSelected(true);
        jrbHtml.setText("<html>&lt;html&gt;</html>");
        jrbHtml.setActionCommand("html");
        jrbHtml.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0));
        jrbHtml.setMargin(new java.awt.Insets(0, 0, 0, 0));

        bgRootTag.add(jrbComposition);
        jrbComposition.setText("<ui:composition>");
        jrbComposition.setActionCommand("composition");
        jrbComposition.setBorder(javax.swing.BorderFactory.createEmptyBorder(0, 0, 0, 0));
        jrbComposition.setMargin(new java.awt.Insets(0, 0, 0, 0));

        java.util.ResourceBundle bundle = java.util.ResourceBundle.getBundle("org/netbeans/modules/web/jsf/wizards/Bundle"); // NOI18N
        jlRootTag.setText(bundle.getString("LBL_RootTag")); // NOI18N

        jlTemplate.setText(bundle.getString("LBL_SelectTemplate")); // NOI18N

        jtfTemplate.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyReleased(java.awt.event.KeyEvent evt) {
                jtfTemplateKeyReleased(evt);
            }
        });

        jbBrowse.setMnemonic(java.util.ResourceBundle.getBundle("org/netbeans/modules/web/jsf/wizards/Bundle").getString("MNE_Browse").charAt(0));
        jbBrowse.setText(bundle.getString("LBL_Browse")); // NOI18N
        jbBrowse.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jbBrowseActionPerformed(evt);
            }
        });

        sectionsToGenerateLabel.setText(org.openide.util.NbBundle.getMessage(TemplateClientPanelVisual.class, "LBL_SectionsToGenerate")); // NOI18N

        sectionsTable.setModel(getNoTemplateTableModel());
        sectionsTable.setEnabled(false);
        sectionsTable.setTableHeader(null);
        SectionsScrollPane.setViewportView(sectionsTable);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
        this.setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jlTemplate)
                    .addGroup(layout.createSequentialGroup()
                        .addGap(138, 138, 138)
                        .addComponent(jtfTemplate, javax.swing.GroupLayout.DEFAULT_SIZE, 313, Short.MAX_VALUE)))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jbBrowse))
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(jlRootTag)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                        .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                            .addComponent(jrbComposition)
                            .addComponent(jrbHtml, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                    .addComponent(sectionsToGenerateLabel))
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
            .addComponent(SectionsScrollPane)
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(jlTemplate)
                    .addComponent(jbBrowse)
                    .addComponent(jtfTemplate, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jlRootTag)
                    .addComponent(jrbHtml, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(jrbComposition)
                .addGap(18, 18, 18)
                .addComponent(sectionsToGenerateLabel)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(SectionsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 79, Short.MAX_VALUE))
        );
    }// </editor-fold>//GEN-END:initComponents
    
    private void jtfTemplateKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_jtfTemplateKeyReleased
        //validateTemplate(new File(jtfTemplate.getText()));
        templateData = Collections.EMPTY_SET;
        fireChangeEvent();
    }//GEN-LAST:event_jtfTemplateKeyReleased

    @Messages({
        "TemplateClientPanelVisual.lbl.resource.library.contract=Resource Library Contract",
        "TemplateClientPanelVisual.lbl.web.pages=Web Pages"
    })
    private void jbBrowseActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jbBrowseActionPerformed
        String projectDirPath = Templates.getProject(wizardDescriptor).getProjectDirectory().getPath();
        final boolean projectNamedContracts = projectDirPath.contains(JsfConstants.CONTRACTS_FOLDER);
        BrowseFolders bf = new BrowseFolders(getFaceletTemplateRoots(), new BrowseFolders.Naming() {
            @Override
            public String getName(String path, String folderName) {
                boolean isContract;
                if (projectNamedContracts) {
                    String[] split = path.split(JsfConstants.CONTRACTS_FOLDER);
                    isContract = split.length > 2;
                } else {
                    isContract = path.contains(JsfConstants.CONTRACTS_FOLDER);
                }
                if (isContract) {
                    return Bundle.TemplateClientPanelVisual_lbl_resource_library_contract() + " : " + folderName; //NOI18N
                } else {
                    return Bundle.TemplateClientPanelVisual_lbl_web_pages();
                }
            }
        });
        org.openide.filesystems.FileObject fo = bf.showDialog();
        if (fo != null) {
            String path = fo.getPath();
            // presume resource library contract
            if (path.contains(JsfConstants.CONTRACTS_FOLDER)) {
                jtfTemplate.setText(getRelativePathInsideResourceLibrary(path));
            } else {
                jtfTemplate.setText(path);
            }
            templateData = Collections.EMPTY_SET;
        }
        fireChangeEvent();
    }//GEN-LAST:event_jbBrowseActionPerformed

    private FileObject[] getFaceletTemplateRoots() {
        List<FileObject> roots = new ArrayList<FileObject>();
        // web project document roots
        roots.addAll(getSourceRoots());
        // web project contracts roots
        roots.addAll(getProjectContractsRoots());
        // libraries contracts roots
        roots.addAll(getLibContractsRoots());

        return roots.toArray(new FileObject[0]);
    }

    private SourceGroup[] getProjectDocumentSourceGroups() {
        Sources sources = ProjectUtils.getSources(Templates.getProject(wizardDescriptor));
        SourceGroup[] docSourceGroups = sources.getSourceGroups(WebProjectConstants.TYPE_DOC_ROOT);
        SourceGroup[] sourceGroups;
        if (docSourceGroups.length == 0) {
            sourceGroups = sources.getSourceGroups(JavaProjectConstants.SOURCES_TYPE_JAVA);
        } else {
            sourceGroups = docSourceGroups;
        }
        return sourceGroups;
    }

    /**
     * Gets templates from the document roots of the project.
     */
    private List<FileObject> getSourceRoots() {
        List<FileObject> roots = new ArrayList<FileObject>();
        for (SourceGroup sourceGroup : getProjectDocumentSourceGroups()) {
            roots.add(sourceGroup.getRootFolder());
        }
        return roots;
    }

    /**
     * Gets templates from contracts folders of the external .JARs.
     */
    private List<FileObject> getLibContractsRoots() {
        List<FileObject> roots = new ArrayList<FileObject>();
        if (getProjectDocumentSourceGroups().length > 0) {
            ClassPathProvider cpp = Templates.getProject(wizardDescriptor).getLookup().lookup(ClassPathProvider.class);
            for (SourceGroup sourceGroup : getProjectDocumentSourceGroups()) {
                ClassPath cp = cpp.findClassPath(sourceGroup.getRootFolder(), ClassPath.COMPILE);
                if (cp != null) {
                    for (FileObject root : cp.getRoots()) {
                        FileObject contracts = root.getFileObject("META-INF/" + JsfConstants.CONTRACTS_FOLDER); //NOI18N
                        if (contracts != null && contracts.isValid() && contracts.isFolder()) {
                            for (FileObject contract : contracts.getChildren()) {
                                roots.add(contract);
                            }
                        }
                    }
                }
            }
        }
        return roots;
    }

    /**
     * Gets templates from contracts folders of the document roots.
     */
    private List<FileObject> getProjectContractsRoots() {
        List<FileObject> roots = new ArrayList<FileObject>();
        for (SourceGroup sourceGroup : getProjectDocumentSourceGroups()) {
            FileObject contractsRoot = sourceGroup.getRootFolder().getFileObject(JsfConstants.CONTRACTS_FOLDER);
            if (contractsRoot != null && contractsRoot.isValid() && contractsRoot.isFolder()) {
                for (FileObject fileObject : contractsRoot.getChildren()) {
                    if (fileObject.isValid() && fileObject.isFolder()) {
                        roots.add(fileObject);
                    }
                }
            }
        }
        return roots;
    }

    /**
     * Gets relative path inside the resource library contract if available. Otherwise it returns the entered full path.
     * @param fullPath to detect inside contracts
     * @return relative path within contract if any, full path otherwise
     */
    protected static String getRelativePathInsideResourceLibrary(String fullPath) {
        if (!fullPath.contains(JsfConstants.CONTRACTS_FOLDER)) {
            return fullPath;
        }
        int rootIndex = fullPath.lastIndexOf(JsfConstants.CONTRACTS_FOLDER) + JsfConstants.CONTRACTS_FOLDER.length() + 1;
        int nextSlashOffset = fullPath.indexOf("/", rootIndex); //NOI18N
        // root folder selected
        if (nextSlashOffset != -1) {
            String resourceLibraryName = fullPath.substring(rootIndex, nextSlashOffset);
            return fullPath.substring(fullPath.indexOf(resourceLibraryName) + resourceLibraryName.length());
        }
        return fullPath;
    }

    private FileObject obtainContractTemplate(String path) {
        List<FileObject> contractsRoots = getProjectContractsRoots();
        contractsRoots.addAll(getLibContractsRoots());
        for (FileObject contract : contractsRoots) {
            FileObject fileObject = contract.getFileObject(path);
            if (fileObject != null && fileObject.isValid() && !fileObject.isFolder()) {
                return fileObject;
            }
        }
        return null;
    }

    protected boolean validateTemplate() {
        if (templateData != null && !templateData.isEmpty()) {
            return true;
        }
        String message = null;
        String path = jtfTemplate.getText();
        if (path == null || "".equals(path)) {
            message = "MSG_NoTemplateSelected"; //NOI18N
        } else {
            File file = new File(path);
            if (file.exists()) {
                if (!file.isDirectory()) {
                    FileObject fo = FileUtil.toFileObject(file);
                    parseTemplateData(fo);
                    if (templateData == null || templateData.isEmpty()) {
                       message = "MSG_NoFaceletsTemplate"; //NOI18N
                    }
                } else {
                    message = "MSG_TemplateHasToBeFile";   //NOI18N
                }
            } else {
                FileObject contractFO = obtainContractTemplate(path);
                if (contractFO == null) {
                    message = "MSG_EneterExistingTemplate";    //NOI18N
                } else {
                    parseTemplateData(contractFO);
                    if (templateData == null || templateData.isEmpty()) {
                       message = "MSG_NoFaceletsTemplate"; //NOI18N
                    }
                }
            }
        }
        if (message != null){
            wizardDescriptor.putProperty(WizardDescriptor.PROP_ERROR_MESSAGE,        //NOI18N
                    NbBundle.getMessage(TemplateClientPanelVisual.class, message));
        }

        if (message == null) {
            return true;
        } else {
            resetSectionsTable();
            return false;
        }
    }

    private void parseTemplateData(FileObject fo) {
        Parameters.notNull("fileObject", fo); //NOI18N
        Source source = Source.create(fo);
        try {
            ParserManager.parse(Collections.singleton(source), new UserTask() {
                @Override
                public void run(ResultIterator resultIterator) throws Exception {
                    templateData = new LinkedHashSet<>();
                    Result result = resultIterator.getParserResult(0);
                    if (result.getSnapshot().getMimeType().equals("text/html")) {
                        HtmlParserResult htmlResult = (HtmlParserResult)result;
                        String ns = DefaultLibraryInfo.FACELETS.getValidNamespaces().stream()
                                .filter(htmlResult.getNamespaces()::containsKey)
                                .findFirst()
                                .orElse(null);
                        if (ns == null) {
                            return;
                        }
                        String faceletsPrefix = htmlResult.getNamespaces().get(ns);
                        List<OpenTag> foundNodes = findValue(htmlResult.root(ns).children(OpenTag.class), faceletsPrefix + ":insert", new ArrayList<OpenTag>()); // NOI18N

                        foundNodes.stream()
                                .map(node -> node.getAttribute(VALUE_NAME))
                                .filter(Objects::nonNull)
                                .map(Attribute::unquotedValue)
                                .filter(value -> value != null && !"".equals(value))
                                .map(CharSequence::toString)
                                .forEach(templateData::add);
                    }
                }
            });
        } catch (ParseException ex) {
            Exceptions.printStackTrace(ex);
        }

        if (!templateData.isEmpty()) {
            loadSectionsTable();
        } else {
            resetSectionsTable();
        }
    }

    private List<OpenTag> findValue(Collection<OpenTag> nodes, String tagName, List<OpenTag> foundNodes) {
        if (nodes == null) {
            return foundNodes;
        }
        for (OpenTag ot : nodes) {
            if(LexerUtils.equals(tagName, ot.name(), true, false)) {
                foundNodes.add(ot);
            } else {
                foundNodes = findValue(ot.children(OpenTag.class), tagName, foundNodes);
            }

        }
        return foundNodes;
    }
    
    @Override
    public HelpCtx getHelpCtx() {
        return new HelpCtx("org.netbeans.modules.web.jsf.wizards.TemplateClientPanelVisual");
    }
    
    public InputStream getTemplateClient(){
        String path = "org/netbeans/modules/web/jsf/facelets/resources/templates/";  //NOI18N
        path = path + bgRootTag.getSelection().getActionCommand() + "TemplateClient.template";          //NOI18N
        InputStream is = this.getClass().getClassLoader().getResourceAsStream(path);
        return is;
    }

    Collection<String> templateData = Collections.EMPTY_SET;
    
    public Collection<String> getTemplateData(){
        return templateData;
    }

    public Collection<String> getTemplateDataToGenerate() {
        if (sectionsTable.getModel() instanceof SectionsTableModel) {
            List<String> sections = new ArrayList<>();
            for (int i = 0; i < sectionsTable.getRowCount(); i++) {
                if ((Boolean) sectionsTable.getValueAt(i, 0)) {
                    sections.add((String) sectionsTable.getValueAt(i, 1));
                }
            }
            return sections;
        } else {
            return getTemplateData();
        }
    }

    public TemplateEntry getTemplate() {
        TemplateEntry templateEntry = new TemplateEntry(null);
        String path = jtfTemplate.getText();
        if (path != null && !"".equals(path)) {
            File file = new File(path);
            FileObject template = FileUtil.toFileObject(file);
            if (template == null || !template.isValid() || template.isFolder()) {
                template = obtainContractTemplate(path);
                templateEntry = new TemplateEntry(template, true);
            } else {
                templateEntry = new TemplateEntry(template);
            }
        }
        return templateEntry;
    }
    
    protected void addChangeListener(ChangeListener l) {
        synchronized (listeners) {
            listeners.add(l);
        }
    }
    
    protected void removeChangeListener(ChangeListener l) {
        synchronized (listeners) {
            listeners.remove(l);
        }
    }
    
    protected final void fireChangeEvent() {
        Iterator it;
        synchronized (listeners) {
            it = new HashSet(listeners).iterator();
        }
        ChangeEvent ev = new ChangeEvent(this);
        while (it.hasNext()) {
            ((ChangeListener)it.next()).stateChanged(ev);
        }
    }

    @Messages({
        "TemplateClientPanelVisual.lbl.select.valid.template=Select valid or non-empty template..."
    })
    private static TableModel getNoTemplateTableModel() {
        TableModel model = new DefaultTableModel(1, 1);
        model.setValueAt(Bundle.TemplateClientPanelVisual_lbl_select_valid_template(), 0, 0);
        return model;
    }

    private void resetSectionsTable() {
        sectionsTable.setEnabled(false);
        sectionsTable.setModel(getNoTemplateTableModel());
        sectionsTable.repaint();
    }

    private void loadSectionsTable() {
        sectionsTable.setEnabled(true);

        // Setup table model of the section's table
        TableModel model = new SectionsTableModel(templateData.size(), 2);
        int index = 0;
        for (Iterator<String> it = templateData.iterator(); it.hasNext();) {
            String section = it.next();
            model.setValueAt(true, index, 0);
            model.setValueAt(section, index, 1);
            index++;
        }
        sectionsTable.setModel(model);

        // Setup column widths
        sectionsTable.getColumnModel().getColumn(0).setPreferredWidth(50);
        sectionsTable.getColumnModel().getColumn(1).setPreferredWidth(300);

        sectionsTable.repaint();
    }
    
    // Variables declaration - do not modify//GEN-BEGIN:variables
    private javax.swing.JScrollPane SectionsScrollPane;
    private javax.swing.ButtonGroup bgRootTag;
    private javax.swing.JButton jbBrowse;
    private javax.swing.JLabel jlRootTag;
    private javax.swing.JLabel jlTemplate;
    private javax.swing.JRadioButton jrbComposition;
    private javax.swing.JRadioButton jrbHtml;
    private javax.swing.JTextField jtfTemplate;
    private javax.swing.JTable sectionsTable;
    private javax.swing.JLabel sectionsToGenerateLabel;
    // End of variables declaration//GEN-END:variables

    private static class SectionsTableModel extends DefaultTableModel {
        private static final long serialVersionUID = 1L;

        public SectionsTableModel(int rowCount, int columnCount) {
            super(rowCount, columnCount);
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            if (columnIndex == 0) {
                return Boolean.class;
            } else {
                return super.getColumnClass(columnIndex);
            }
        }

        @Override
        public boolean isCellEditable(int row, int col) {
            return (col == 0);
        }

    }

}
